# -*- coding: utf-8 -*-
#@Time    :2019/12/21 20:07
#@Author  :zhaoyijie（Aquarius_genius）水瓶座鬼才
#@Email   : 747897928@qq.com
#@FileName: M3u8Download6.0plus.py
#@Software: PyCharm
import os
import sys
import requests
from urllib.parse import urlparse
from concurrent.futures import ThreadPoolExecutor#线程池
from urllib3.exceptions import InsecureRequestWarning
import time
import re
import json
#用于将windows系统不支持的非法文件名替换成合法的文件名
def validateTitle(title):
    return re.sub(r"[\/\\\:\*\?\"\<\>\|]" , "_", title).replace(' ','') # 替换为下划线 # '/ \ : * ? " < > |'
#判断字符串是否存在中文
def check_contain_chinese(check_str):
    zhPattern = re.compile(u'[\u4e00-\u9fa5]+')
    match = zhPattern.search(check_str)
    if match:
        return True
    else:
        return False
#os.remove(path)  # path是文件的路径，如果这个路径是一个文件夹，则会抛出OSError的错误，这时需用用rmdir()来删除
#os.rmdir(path)  # path是文件夹路径，注意文件夹需要时空的才能被删除
#os.unlink('F:\新建文本文档.txt')  # unlink的功能和remove一样是删除一个文件，但是删除一个删除一个正在使用的文件会报错。
def delete_Ts_files(path):
    file_list=file_walker(path)
    for i in range(len(file_list)):
        os.remove(os.path.join(path,file_list[i]))
    os.rmdir(path)
    print("delete temp dir")
#创建文件夹
def mkdir(path):
    if not os.path.exists(path):
        os.makedirs(path)
#获取文件夹下所有的文件名，按顺序
def file_walker(path):
    #获取文件夹下所有的文件名
    file_list=os.listdir(path)
    #将文件名按.切割，比如:文件名为1.ts得到类似于x.split('.')[0]=1   x.split('.')[1]=ts
    # 用lamdba表达式和sort排序文件名,将切割数组的第一个元素强制类型转为int再比较
    file_list.sort(key=lambda x: int(x.split('.')[0]))
    #返回一个语句排序过文件名的数组
    return file_list
def combine(ts_dir_path,Save_file_name,userdir_cwd):
    current_time=str(int(time.time()))
    mkdir('output')
    global TSformatMP4
    if TSformatMP4:
        # 由于路径中有空格，所以路径需要用上双引号，否则会找不到该文件
        # mp4文件保存的绝对路径
        absolute_mp4_save_path = os.path.join(userdir_cwd, 'output',Save_file_name + '.mp4')
        # 因为ffmpeg指令不支持中文路径，所以如果用户输入的文件名为中文，就得先用ffmpeg合成并保存为临时的mp4文件再重命名这个mp4文件
        temp_absolute_mp4_save_path = os.path.join(userdir_cwd, 'output',current_time + '.mp4')
        #之前修改过内容保存的m3u8文件的绝对路径
        absolute_SaveM3u8Path=os.path.join(userdir_cwd,ts_dir_path,ts_dir_path+'.m3u8')
        #windows系统的路径用\而不是/，如果不是/路径而是\路径FFmpeg根据m3u8找ts文件时
        # 就会找不到ts文件的位置从而报错Invalid data found when processing input
        if sys.platform == 'win32':
            absolute_SaveM3u8Path = absolute_SaveM3u8Path.replace("\\", '/')
        print(absolute_SaveM3u8Path)
        #'ffmpeg -i {} -vcodec copy -acodec copy {}'
        #mp4_compound_instruct = 'ffmpeg -i {} -c copy -bsf:a aac_adtstoasc {}'.format(absolute_SaveM3u8Path,temp_absolute_mp4_save_path)
        mp4_compound_instruct = 'ffmpeg -i {} -vcodec copy -acodec copy {}'.format(absolute_SaveM3u8Path,temp_absolute_mp4_save_path)
        print(mp4_compound_instruct)
        os.system(mp4_compound_instruct)
        oldname = temp_absolute_mp4_save_path  # 老文件夹的名字
        newname = absolute_mp4_save_path  # 新文件夹名字
        # 重命名mp4文件名
        os.rename(oldname, newname)
        # 删除ts文件下载的临时文件夹
        delete_Ts_files(ts_dir_path)
    else:
        #将ts存放的文件夹重命名
        os.rename(ts_dir_path, Save_file_name)
#以一个绝对路径作为参照补全一个相对路径
def Resultant_absolute_path(absolute_url,relative_path):
    #某种m3u8文件，比如https://youku.com-iqiyi.net/20190823/22550_41c5b03c/1000k/hls/index.m3u8
    #里面的ts文件是这样，64d9f19501b000000.ts ，但是绝对路径是
    # https://youku.com-iqiyi.net/20190823/22550_41c5b03c/1000k/hls/64d9f19501b000000.ts
    if '/' not in relative_path:
        last_index=absolute_url.rfind('/')
        return absolute_url[:last_index+1]+relative_path
    else:
        #https://www.mzy2000.com:65/20190822/SDn5dz82/2000kb/hls/index.m3u8
        #里面的内容是/20190822/SDn5dz82/2000kb/hls/KjNd3900000.ts，绝对路径是
        #https://www.mzy2000.com:65/20190822/SDn5dz82/2000kb/hls/KjNd3900000.ts
        #我们补全只需要https://www.mzy2000.com:65加上/20190822/SDn5dz82/2000kb/hls/KjNd3900000.ts就行了
        split_url = urlparse(absolute_url)
        ts_absolute_path = split_url.scheme + '://' + split_url.netloc + relative_path
        return ts_absolute_path
#该方法用来判断m3u8是否是内嵌的m3u8
def isNestedM3U8(file_line):
    #比如：https://www.mzy2000.com:65/20190906/Rx59kbUO/index.m3u8就是内嵌m3u8
    #其里面的内容为
    # #EXTM3U
    #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2000000,RESOLUTION=1080x608
    #/20190906/Rx59kbUO/2000kb/hls/index.m3u8
    for lineitem in file_line:
        #如果里面存在/20190906/Rx59kbUO/2000kb/hls/index.m3u8这种情况返回这个字符串
        if '.m3u8' in lineitem and '.ts' not in lineitem:
            print(lineitem)
            return  lineitem
        elif '.ts'in lineitem:
            return ''
        # 如果存在类似于https://www.mmicloud.com:65/20190315/Rq1ZIwKY/index.m3u8这种情况
        # 内容是#EXTM3U
        ##EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2000000,RESOLUTION=1080x456
        # /ppvod/e2lnG5BP
        # 我们需要拿到/ppvod/e2lnG5BP去合成https://www.mmicloud.com:65/ppvod/e2lnG5BP
        elif '/' in lineitem:
            print(lineitem)
            return lineitem
        else:
            pass
    return ''
def getM3u8contentList(url):
    global headers
    all_content = requests.get(url=url, headers=headers,verify=False).text  # 获取M3U8的文件内容
    file_line = all_content.split("\n")  # 读取文件里的每一行
    return file_line
def download_M3U8(url,download_path):
    file_line =getM3u8contentList(url)
    NestedM3U8url=isNestedM3U8(file_line)

    #如果这个m3u8是内嵌型的m3u8
    if NestedM3U8url!='':
        #拼接这个m3u8的绝对路径，也就是将/20190906/Rx59kbUO/2000kb/hls/index.m3u8变成
        # https://www.mzy2000.com:65/20190906/Rx59kbUO/2000kb/hls/index.m3u8
        url=Resultant_absolute_path(url,NestedM3U8url)
        print('内嵌m3u8型的m3u8，真实路径为：',url)
        file_line=getM3u8contentList(url)
    #修改链接里的m3u8的内容并将新的内容写入到本地作为新的m3u8文件，后面的ts合成得靠这个新的m3u8
    #新的m3u8内容字符串
    SaveM3u8Content = ''
    # 通过判断文件头来确定是否是M3U8文件
    if file_line[0] != "#EXTM3U":
        raise BaseException(u"非M3U8的链接")
    else:
        TS_List=[]
        unknow = True  # 用来判断是否找到了下载的地址
        for index, line in enumerate(file_line):
            TempLineItem=line
            if "EXTINF" in line:
                unknow = False
                # 拼出ts片段的URL
                ts_url=file_line[index + 1]
                #如果这个ts的路径存在hhtp，那应该是一个绝对路径，就不补全路径
                if 'http' in ts_url:
                    pass
                else:
                    ts_url=Resultant_absolute_path(url,ts_url)
                #下载的ts文件保存路径
                savepath = download_path + "/" + str(index) + '.ts'
                #把这一行和下一行的内容拼接
                #类似于#EXTINF:....,加上换行符和4.ts
                TempLineItem=line+'\n'+str(index) + '.ts'
                TS_dict = {}
                TS_dict['savepath'] =savepath
                TS_dict['ts_url'] = ts_url
                TS_List.append(TS_dict)
            if '#' in line:
                SaveM3u8Content=SaveM3u8Content+TempLineItem+'\n'
        if unknow:
            raise BaseException("未找到对应的下载链接")
        else:
            mkdir(download_path)
            SaveM3u8Path='{}/{}.m3u8'.format(download_path,download_path)
            with open(SaveM3u8Path,"wt", encoding='utf-8') as fw:
                fw.write(SaveM3u8Content)
            return TS_List
#用来处理ts的下载和io保存的方法
def requestsAndIO(ts_url,savepath,timeoutnumber):
    try:
        global headers
        res = requests.get(url=ts_url, headers=headers,verify=False,timeout=(timeoutnumber, timeoutnumber))
        if res.status_code == 404:
            print('404 not found', ts_url)
            res.close()
            return
        if res.status_code == 403:
            print('403 服务器拒绝请求', ts_url)
            res.close()
            return
        with open(savepath, 'wb') as f:
            f.write(res.content)
            f.flush()
        res.close()
        print(ts_url)
        return True
    except Exception as err:
        print(err,ts_url," 下载失败，正在尝试重新下载")
        return False
def download_TS(TS_dict):
    savepath=TS_dict['savepath']
    ts_url=TS_dict['ts_url']
    timeoutnumber=60#默认超时时间
    #尝试下载某个ts片段，最多重试9次
    for i in range(0, 10):
        timeoutnumber = timeoutnumber + 60#每次下载都会增加超时的等待时间
        #如果下载成功就会返回True，如果为True就跳出循环结束这个ts下载，如果下载失败就加超时等待时间，继续尝试重新下载
        if(requestsAndIO(ts_url, savepath, timeoutnumber)):
             break
def main(M3u8_url,Save_file_name):
    global userdir_cwd
    tempfilename = str(int(time.time()))  # 批量的TS文件下载存放的临时文件夹，time.time()当前时间的毫秒数
    # 将用户输入的windows系统非法字符替换
    TS_List = download_M3U8(M3u8_url, tempfilename)
    # 如果ts数组不是空的
    if TS_List:
        print('TS文件数量：', len(TS_List))
        # 开启线程池迭代ts数组下载，线程池大小为5
        with ThreadPoolExecutor(max_workers=5) as executor:
            executor.map(download_TS, TS_List)
        # 合成ts文件
        combine(tempfilename, Save_file_name, userdir_cwd)
def IO_JSON_File():
    with open('task.json', 'r', encoding='utf-8') as f:
        Json_content=f.read()
    #防止用户将逗号输成中文逗号，将数组的双引号输成单引号。
    Json_content=Json_content.replace('\'',"\"").replace("，",",")
    Json_content=json.loads(Json_content)
    M3u8_List = Json_content['taskList']
    Save_file_base_name= Json_content['SaveName']
    TaskCompletedShutdown=Json_content['TaskCompletedShutdown']
    TSformatMP4=Json_content['TSformatMP4']
    return M3u8_List,Save_file_base_name,TaskCompletedShutdown,TSformatMP4
def createJsonFile():
    if os.path.isfile('task.json'):
        pass
    else:
        with open('task.json', "wt", encoding='utf-8') as f:
            f.write('{\n"taskList":[],\n"SaveName":"",\n"TaskCompletedShutdown":false,\n"TSformatMP4":true\n}')
def ShutDownComputer():
    print('任务下载完成，四秒后关闭计算机')
    #os.system('shutdown /s /t 0')
    #Windows
    time.sleep(4)
    if sys.platform=='win32':
        os.system('shutdown /s /t 0')
    #linux
    elif sys.platform=='linux':
        os.system('shutdown -h now')
    #Mac OS X
    elif sys.platform=='darwin':
        os.system('sudo shutdown -h now')
    else:
        print('未知操作系统,无法关机')
if __name__ == '__main__':
    print(sys.platform)
    os.system('title ' + 'M3u8-TS批量下载器 Made by 水瓶座鬼才')
    headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36"}
    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
    userdir_cwd = os.getcwd()  # os.getcwd() 方法用于返回当前工作目录。
    # ffmpeg不支持在中文文件夹，所有不支持在中文文件夹下运行程序
    if check_contain_chinese(userdir_cwd):
        print('不支持在中文文件夹下运行程序，程序将在6秒钟退出,')
        time.sleep(6)
    else:
        createJsonFile()
        #python中这种用法并非一个方法返回多个值，这是不存在的，这种用法是返回一个元组，相当于返回
        #(M3u8_List, Save_file_base_name, TaskCompletedShutdown, TSformatMP4)这个元组
        M3u8_List, Save_file_base_name, TaskCompletedShutdown, TSformatMP4= IO_JSON_File()
        print(M3u8_List, Save_file_base_name, TaskCompletedShutdown, TSformatMP4,len(M3u8_List))
        Save_file_base_name = validateTitle(Save_file_base_name)
        if M3u8_List and Save_file_base_name!="":
            mkdir('output')
            for M3u8_List_index, M3u8_url in enumerate(M3u8_List):
                # 比如基础名为滨虎，第一集的mp4就为滨虎1，第二集为滨虎2,以此类推
                Save_file_name = Save_file_base_name+ str(M3u8_List_index+1)
                if os.path.isfile('output/{}.mp4'.format(Save_file_name)):
                    print('output文件夹存在此文件，疑似已下载，跳过该文件下载')
                else:
                     main(M3u8_url, Save_file_name)
        if TaskCompletedShutdown:
            ShutDownComputer()






